package com.iwjw.ibatis.plugins.explain;

import com.iwjw.ibatis.plugins.PluginUtil;
import com.iwjw.ibatis.plugins.page.PagePlugin;
import com.iwjw.ibatis.util.ReflectionUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * SQL执行分析器
 *
 * @author: huangweiwei
 * @email: ihuangweiwei@163.com
 * @created:2016/11/3
 */
@Intercepts({
        @Signature(type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class})})
public class ExplainPlugin implements Interceptor {

    protected static final Logger logger = LoggerFactory.getLogger(PagePlugin.class);
    /**
     * SQL执行的type底线
     */
    private ExplainSqlType baseLine = ExplainSqlType.REF;

    private Integer middleTableRows = 500;

    private Integer indexRows = 1000;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MetaObject metaStatementHandler = PluginUtil.getMetaObject((StatementHandler) invocation.getTarget());
        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        //插入操作不做性能分析
        if (boundSql.getSql().toLowerCase().contains("insert")) {
            return invocation.proceed();
        }
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        explain(boundSql.getSql(), (Connection) invocation.getArgs()[0], mappedStatement, boundSql);
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    @Override
    public void setProperties(Properties properties) {
        String middleTableRows = properties.getProperty("middleTableRows", "500");
        String indexRows = properties.getProperty("indexRows", "1000");
        String baseType = properties.getProperty("baseType", ExplainSqlType.REF.name());
        ExplainSqlType explainSqlType = ExplainSqlType.get(baseType);
        if (explainSqlType != null) {
            this.baseLine = explainSqlType;
        }
        this.middleTableRows = Integer.valueOf(middleTableRows);
        this.indexRows = Integer.valueOf(indexRows);
    }

    private void explain(String sql, Connection connection, MappedStatement mappedStatement, BoundSql boundSql) throws Throwable {
        List<Explain> explainList = new ArrayList<Explain>();
        StringBuffer explainSql = new StringBuffer();
        explainSql.append("explain ").append(sql);
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = connection.prepareStatement(explainSql.toString());
            BoundSql newBoudSql = new BoundSql(mappedStatement.getConfiguration(), explainSql.toString(),
                    boundSql.getParameterMappings(), boundSql.getParameterObject());

            PluginUtil.addMetaParametersAndAdditionParameters(newBoudSql,boundSql);

            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,
                    boundSql.getParameterObject(), newBoudSql);
            parameterHandler.setParameters(pstmt);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                Explain explain = new Explain(
                        rs.getInt("id"), rs.getString("select_type"), rs.getString("table"),
                        rs.getString("type"), rs.getString("possible_keys"), rs.getString("key"),
                        rs.getInt("key_len"), rs.getString("ref"), rs.getInt("rows"), rs.getString("Extra")
                );
                explainList.add(explain);
            }
            //如果检测不OK，需要打印信息
            if (!analyzeOk(explainList)) {
                String mapperId = mappedStatement.getId();
                String executeSql =PluginUtil.showSql(mappedStatement.getConfiguration(),boundSql);
                //TODO 格式化出explainList的字符串
                logger.error("\nMapperId:{};\nSQL:{};\nexplain:{}\n", mapperId, executeSql.toString(), explainList.toString());
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (pstmt != null) {
                    pstmt.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 策略：
     * table：
     * 不存在：那就无需分析
     * 存在：分析返回的type类型
     * 低于预期：判断返回结果的条数操作
     * 高于预期：判断ref是否为null
     *
     * @param explainList
     * @return
     */
    private boolean analyzeOk(List<Explain> explainList) {
        for (Explain explain : explainList) {
            //表都没有，还分析啥
            if (explain.getTable() == null) {
                continue;
            }
            ExplainSqlType explainSqlType = ExplainSqlType.get(explain.getType());
            boolean isBreak = ExplainSqlType.isBreakBaseLine(explainSqlType, baseLine);
            //中间表和非中间表的逻辑判断
            //非中间表
            if (!explain.getTable().startsWith("<")) {
                if (isBreak) {
                    logger.error("type太low,规定:{},实际:{}", baseLine.name(), explainSqlType.name());
                    return false;
                }
                //符合type要求，那就需要判断有没有ref
                if (explain.getRef() == null) {
                    logger.error("有索引,但真实情况ref==null");
                    return false;
                }
                //TODO 是否设置索引查询的大小
                if (explain.getRef() != null && explain.getRows() > indexRows) {
                    logger.error("索引列超出指定条数,规定:{},实际:{}", indexRows, explain.getRows());
                    return false;
                }
                continue;
            }
            //中间表，只能通过返回的条数去判断
            if (explain.getRows() == null || explain.getRows() <= middleTableRows) {
                continue;
            }
            logger.error("中间表超出指定条数,规定:{},实际:{}", middleTableRows, explain.getRows());
            //超出阙值，直接返回
            return false;
        }
        return true;
    }


}
